Angular 19 中引入的新 LinkedSignal
功能透過允許訊號 (signal) 直接連結到來源值,提供了管理反應狀態 (reactive state) 的強大機制。 LinkedSignal
創造一個 WritableSignal
;因此,開發人員可以明確設定該值或在來源變更時更新該值,從而促進兩者之間的無縫同步。
這篇部落格文章透過四個範例來展示 LinkedSignal 的功能。
asReadOnly
方法傳回一個 Signal,並將其傳回給元件進行顯示。<div>
<button (click)="pageNumber.set(1)">First</button>
<button (click)="changePageNumber(-1)">Prev</button>
<button (click)="changePageNumber(1)">Next</button>
<button (click)="pageNumber.set(200)">Last</button>
<p>Go to: <input type="number" [(ngModel)]="pageNumber" /></p>
</div>
<p>Page Number: {{ pageNumber() }}</p>
<p>Current Page Number {{ currentPageNumber() }}</p>
<p>Percentage of completion: {{ percentageOfCompletion() }}</p>
The template has four buttons that set the pageNumber signal to 1, decrease the signal by 1, increase the signal by 1, and set the signal to 200. The number input directly writes the value to the same signal. The template also displays the value of the pageNumber signal, currentPageNumber linked signal, and the percentableOfCompletion computed signal.
此範本有四個按鈕,分別將 pageNumber
訊號設定為 1、將訊號減少 1、將訊號增加 1 以及將訊號設定為 200。 Number input field 直接將值寫入 pageNumber
訊號。 此範本還顯示 pageNumber
訊號、currentPageNumber
LinkedSignal 和 percentageOfCompletion
計算訊號的值。
// pagination.component.ts
import { ChangeDetectionStrategy, Component, computed, linkedSignal, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-pagination',
standalone: true,
imports: [FormsModule],
templateUrl:
template: `...the inline template…`
changeDetection: ChangeDetectionStrategy.OnPush
})
export default class PaginationComponent {
pageNumber = signal(1)
currentPageNumber = linkedSignal<number, number>({
source: this.pageNumber,
computation: (pageNumber, previous) => {
if (!previous) {
return pageNumber;
}
return (pageNumber < 1 || pageNumber > 200) ? previous.value : pageNumber
}
});
percentageOfCompletion = computed(() => `${((this.currentPageNumber() * 1.0) * 100 / 200).toFixed(2)}%`);
changePageNumber(offset: number) {
this.pageNumber.update((value) => Math.max(1, Math.min(200, value + offset)));
}
}
currentPageNumber
的來源是 pageNumber
訊號,當來源變更時,該訊號會計算新值。computation
屬性是一個接受頁碼和 previous
物件的函數。當 previous
物件未定義時,LinkedSignal 傳回頁碼。當頁碼超出範圍時,LinkedSignal 將傳回上一個頁碼或 previous.value
。
在示範中,我可以輸入 201 將值綁定到 pageNumber
訊號,但 currentPageNumber
恢復為先前的值。
此外,計算訊號可以從 LinkedSignal 衍生,因為它也是 WritableSignal。 percentageOfCompetation
計算訊號源自 currentPageNumber
LinkedSignal,用於計算百分比並將其轉換為字串。
<h2>Update the shorthand version of the linked signal. Set and update the signal</h2>
<p>Update country: <input [(ngModel)]="country" /></p>
<p>Update favorite country: <input [(ngModel)]="favoriteCountry" /></p>
<button (click)="country.set('United States of America')">Reset</button>
<button (click)="changeCountry()">Update source and linked signal</button>
<p>Country: {{ country() }}</p>
<p>Favorite Country: {{ favoriteCountry() }}</p>
<p>Reversed Country: {{ reversedFavoriteCountry() }}</p>
此模範本兩個 HTML input 元素。 第一個 input field 綁定到 country
訊號,而第二個 input field 綁定到 favoriteCountry
LinkedSignal。 當按鈕重置 country
訊號時,favoriteCountry
也會重置。另一個按鈕呼叫 changeCountry
函數直接寫入 country
訊號和 favoriteCountry
LinkedSignal。 然後,我們顯示訊號以查看每個操作後的不同值。
// favorite-country.component.ts
import { ChangeDetectionStrategy, Component, computed, linkedSignal, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-favorite-country',
standalone: true,
imports: [FormsModule],
template: `... inline template…`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush
})
export default class FavoriteCountryComponent {
country = signal('United States of America')
favoriteCountry = linkedSignal(() => this.country());
reversedFavoriteCountry = computed(() => this.favoriteCountry().split('').toReversed().join(''));
changeCountry() {
this.country.set('Canada');
this.favoriteCountry.update((c) => c.toUpperCase());
}
}
favoriteCountry = linkedSignal(() => this.country());
這是 LinkedSignal 的簡寫形式,傳回 country
訊號的值。 當我在第一個 HTML input 中輸入不同的國家時,country
和 favoriteCountry
都會更新。此外,第二個 HTML input 顯示 favoriteCountry
的最新值。 當我在第二個 HTML input中輸入不同的國家時,僅更新 favoriteCountry
, country
不受影響。此時,LinkedSignal 和來源持有不同的值。 在這兩種情況下,reversedFavoriteCountry
以相反的順序顯示 favoriteCountry
。 當我單擊按鈕呼叫 changeCountry
方法時,我將 country
訊號設定為 "Canada" 並觸發favoriteCountry
LinkedSignal 進行更新。 LinkedSignal 也是 WritableSignal
;我可以呼叫 update
方法將 favoriteCountry
訊號轉換為大寫。 因此,country
的值是 "Canada",favoriteCountry
是 "CANADA", reversedFavoriteCountry
是 "ADNAC"。
<h2>Reset linked signal after updating source</h2>
<p>Source: {{ shoeSizes() }}</p>
<p>Shoe size: {{ currentShoeSize() }}</p>
<p>Shoe index: {{ index() }}</p>
<div>
<button (click)="changeShoeSizes()">Update shoe size source</button>
<button (click)="updateLargestSize()">Set to the largest size</button>
</div>
<label for="shoeSize">
<span>Choose a shoe size: </span>
<select id="shoeSize" name="shoeSize" [(ngModel)]="currentShoeSize">
@for (size of shoeSizes(); track size) {
<option [ngValue]="size">{{ size }}</option>
}
</select>
</label>
此範本顯示 LinkedSignal 的來源,它是數字 array。 currentShoeSize
LinkedSignal 顯示 array 中選定的元素。 index
計算訊號衍生 currentShoeSize
在來源中的索引。 第一個按鈕呼叫 changeShoeSizes
方法來更新來源並導致 currentShoeSize
設定或重置值。 updateLargeSizes
方法將 currentShoeSize
LinkedSignal 設定為 array 的最後一個元素。 最後,範本填入下拉清單以選擇要寫入 currentShoeSize
LinkedSignal 的值。
import { ChangeDetectionStrategy, Component, computed, linkedSignal, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
const SHOE_SIZES = [5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10];
const SHOE_SIZES2 = [4, 5, 6, 7, 8, 9, 10, 11, 12]
@Component({
selector: 'app-shoe-sizes',
standalone: true,
imports: [FormsModule],
template: `...inline template…`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export default class ShoeSizesComponent {
shoeSizes = signal(SHOE_SIZES);
currentShoeSize = linkedSignal<number[], number>({
source: this.shoeSizes,
computation: (options, previous) => {
if (!previous) {
return options[0];
}
return options.includes(previous.value) ? previous.value : options[0];
}
});
index = computed(() => this.shoeSizes().indexOf(this.currentShoeSize()));
changeShoeSizes() {
if (this.shoeSizes()[0] === SHOE_SIZES2[0]) {
this.shoeSizes.set(SHOE_SIZES);
} else {
this.shoeSizes.set(SHOE_SIZES2);
}
}
updateLargestSize() {
const largestSize = this.shoeSizes().at(-1);
if (typeof largestSize !== 'undefined') {
this.currentShoeSize.set(largestSize);
}
}
}
該示範的重要部分是在呼叫 changeShoeSizes
方法後重置 currentShoeSize
。 此方法在 [5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10]
和 [4, 5, 6, 7, 8, 9, 10, 11, 12]
之間切換 shoesSizes
信號並在此過程中更新來源。 然後,currentShoeSizes
LinkedSignal 使用 computation
函數來計算新值。
computation: (options, previous) => {
if (!previous) {
return options[0];
}
return options.includes(previous.value) ? previous.value : options[0];
}
如果 previous
物件未定義 (undefined),則傳回 array 的第一個元素。 如果新 array 中也存在前一個值,則傳回該值。否則,該函數會傳回 array 的第一個元素。
例如,來源為 [4, 5, 6, 7, 8, 9, 10, 11, 12]
,currentShoeSize
為 10。 如果新來源變成 [5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9,5, 10]
,則該值不會重置,因為找到了 10。 如果 currentShoeSize
是 12,則在 array 中找不到它,computation
函數會將值重設為 5,這是 array的第一個元素。
// shoe-sizes.store.ts
import { linkedSignal, signal } from '@angular/core';
const SHOE_SIZES = [5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10];
const SHOE_SIZES2 = [4, 5, 6, 7, 8, 9, 10, 11, 12];
const _shoeSizes = signal(SHOE_SIZES);
const _currentShoeSize = linkedSignal<number[], number>({
source: _shoeSizes,
computation: (options, previous) => {
if (!previous) {
// reset to the first size
return options[0];
}
return options.includes(previous.value) ? previous.value : options[0];
}
});
export const ShoeSizesStore = {
shoeSizes: _shoeSizes.asReadonly(),
currentShoeSize: _currentShoeSize.asReadonly(),
updateShoeSize(value: number) {
_currentShoeSize.set(value);
},
changeShoeSizes() {
if (_shoeSizes()[0] === SHOE_SIZES2[0]) {
_shoeSizes.set(SHOE_SIZES);
} else {
_shoeSizes.set(SHOE_SIZES2);
}
},
updateLargestSize() {
const largestSize = _shoeSizes().at(-1);
if (typeof largestSize !== 'undefined') {
this.updateShoeSize(largestSize);
}
}
}
// shoe-sizes-store.component.ts
import { ShoeSizesStore } from '../stores';
@Component({
selector: 'app-shoe-sizes-store',
standalone: true,
imports: [FormsModule],
template: `
<p>Source: {{ shoeSizes() }}</p>
<p>Shoe size: {{ currentShoeSize() }}</p>
<p>Shoe index: {{ index() }}</p>
<div>
<button (click)="changeShoeSizes()">Update shoe size source</button>
<button (click)="updateLargestSize()">Set to the largest size</button>
</div>
<label for="shoeSize">
<span>Choose a shoe size: </span>
<select id="shoeSize" name="shoeSize" [ngModel]="currentShoeSize()" (ngModelChange)="updateShoeSize($event)">
@for (size of shoeSizes(); track size) {
<option [ngValue]="size">{{ size }}</option>
}
</select>
</label>
`,
})
export default class ShoeSizesStoreComponent {
currentShoeSize = ShoeSizesStore.currentShoeSize;
shoeSizes = ShoeSizesStore.shoeSizes;
index = computed(() => this.shoeSizes().indexOf(this.currentShoeSize()));
constructor() {
this.updateShoeSize(5);
}
updateShoeSize(value: number) {
ShoeSizesStore.updateShoeSize(value);
}
changeShoeSizes = ShoeSizesStore.changeShoeSizes;
updateLargestSize = ShoeSizesStore.updateLargestSize;
}
我將元件的 LinkedSignal 邏輯移至 store。此元件的 constructor 將 LinkedSignal 的值設為 5。
另一個修改是將 [(ngModel)]
分解為 [ngModel] 和 ngModelChange 事件發射器 (event emitter)。 這是因為 currentShoeSize
是唯讀的,我必須呼叫 updateShoeSize
方法來更新 store 中的 #currentShoeSize
LinkedSignal。
LinkedSignal
有一個來源,可以觸發 computation
函數來設定或重置值。computation
函數接受來源和 previous
物件。 它可以使用這兩個參數來執行邏輯以傳回下一個值。LinkedSignal
是一個 WritableSignal
,可以設定和更新值並傳回唯讀訊號 (read-only signa)。LinkedSignal
可以具有與來源不同的值,因為開發人員可以直接為其寫入值。鐵人賽的第 38 天到此結束